提高(微)服务安全的非完全攻略
【编者按】千里之堤毁于蚁穴。服务安全有点类似于机场安检。工作人员会检查每一个人,禁止他们带某些东西上飞机等等,尽管99.9999%的人都不会劫持飞机。而机场采取的所有措施针对的都是0.0001%的情况。因为后果影响非常大。
作者 | Madalin Ilie
译者 | 弯月
软件的安全问题既困难又复杂。许多人认为安全属于正常开发流程之外的东西。通常被视为某些安全人员的责任,他们只需要关注安全,不需要理解我们在复杂的微服务、事件驱动、API优先战略以及云生态系统中快速交付的业务价值。我认为正是由于这种思想,我们很难在早期阶段思考安全问题,找出任何有可能破坏系统的人或事。安全导致复杂的坏境又多了一层复杂。安全不仅仅体现了现代架构的技术复杂性,而且还有很多其他方面的问题,比如快速走向市场、严格的期限、团队内部问题、性能问题、流程太多、会议太多等等。虽然安全非常复杂,但是它并不会在某一天里突然破坏你的系统。可能在经过数月/数年后,才会有人发现漏洞。那么,为什么我们要从第一天起就关注安全呢?话虽没错,第一天就出现安全问题的可能性非常小。我们都希望集中精力完成具有直接价值的功能,而不是预防未来可能出现的问题。然而,关键在于,安全问题可能会导致整个系统瘫痪。这对于你和你的用户都非常不利。
那么,如何在不过度设计安全性和偏执于一切之间取得平衡,同时仍然还能专注于业务价值呢?这其实是一种思维模式,而不是一个单独的关注点。我并不是说每个人都需要成为安全专家以及无所不知。我是说人们应该开发安全软件,就像他们开发软件一样。通过一种方式将引入漏洞的可能性降到最低。
灌输这种思维模式的最佳方式是采用一套标准和实践来培养习惯。继续拿机场安检做类比,你不能让安检人员自行做决定:
“这个人看上去是好人,他的手提行李可以携带剪刀和刀。”
“这位先生看上去严重脱水,他可以携带一大瓶液体上飞机!”
你需要创建一套平等且适用于每个人的规则和程序(即标准)。此外,你还需要制定一套关于如何处理特定情况的指导方针(即实践):如果你发现手提行李中有可疑物品,则可以单独检查。
接下来,我将详细介绍涵盖整个系统发展生命周期的标准和实践。虽然这个列表无法做到详尽(对于有些小节的内容,你可能有更多良好的实践),但我希望能够帮助你思考一些不太常见的情况。
安全的重点
我简单地将安全问题分为了两个主要领域:
基础设施安全:与应用程序在生产中的部署和运行相关的安全问题。
应用程序安全性:与应用程序的实现以及业务背景细节相关的安全问题。
关于如何解决这两个问题,有很多资源:
PCI 安全要求(https://www.pcisecuritystandards.org/):侧重于财务软件,但也可作为其他系统的最佳实践。
安全代码(https://safecode.org/category/resource-publications/):有关开发、测试、架构以及开发运维的通用实践。
有关安全编程的十大最佳实践(https://wiki.sei.cmu.edu/confluence/display/seccode/Top+10+Secure+Coding+Practices):侧重于某些编程语言的安全编程最佳实践,也有一些通用的实践。
OWASP 备忘单系列(https://cheatsheetseries.owasp.org/index.html):覆盖了大多数软件开发领域的备忘单。
上述资源非常全面,包括许多解决系统发展生命周期内有关安全问题的细节和实践。每位开发人员都应该定期做检查,并掌握最新信息。但在实践中,我们很难做到。下面,我将设法总结出我们需要思考的一些重要问题(注意:这些实践与业务领域无关)。虽然这不是一份详尽的清单,也不是灵丹妙药,但它可以帮助你建立坚实的基础,将发生安全问题的可能性降到最低。
解决安全问题
基础设施的安全比较容易解决,因为很多人都会使用产品或云服务。这些产品和服务已经包含了非常好的安全功能,如果你使用Web应用程序防火墙,则可以相信该产品能够保障安全,不需要由你来实现安全。我并不是说这样做会更容易,而是说你有更多控制。
应用程序的安全则更加难以预测。你需要依靠开发人员的技术来实现安全。你需要确保他们不会做愚蠢的事情,比如在源文件中存储明文密码。
以下是我认为能够帮助你建立安全思维方式的实践列表,主要面向常规开发人员。这里的“常规”指的是实际实现代码的人,而不是其他专注于设计、规划或管理的人。这些规则关注的都是构建 (REST) API 时的应用程序安全。虽然表面看起来它们与安全并没有直接的联系,但最终它们都可以减少引入安全问题的可能性。
注意:本文的大多数示例都使用了Java。
标准
如上所述,标准的使用是建立思维方式的主要机制。所有项目都应该采用同一套标准。并非所有人都喜欢标准,有些人可能会认为标准限制了人们的选择和创造力。但我认为,这是一种简单的获得统一性的方法,尤其是当多个团队在同一平台上工作时。建立标准之后,新成员可以更轻松地加入项目,而且还能降低引入bug、不一致或在某些愚蠢的事情(比如空格还是制表符)上发生争执的可能性。我们可以将这些时间节省出来讨论更有意义的问题。标准不一定要涉及很多细节,大多数标准应该说明建立在现有实践之上的原则和选择。
文档
需要考虑的关键事项:
将代码的接口以及API合同写在文档中;
定义文档策略:
整体的文档策略是什么?
项目的 README.md 文件中包含什么?
是否需要更新更广泛的文档?
采用哪些画图工具?
是否使用轻量级架构决策记录?
文档应当存储在Git代码库中?还是采用单独的工具?
如果存储在项目中,推荐的文件夹结构是什么?
通用(微)服务设计指南
需要考虑的关键事项:
使用蓝图/模板/原型作为所有(微)服务的起点;
将蓝图与所有公共库、插件等捆绑在一起,并且必须符合标准;
每个(微)服务必须能够通过运行一个命令来启动;
(微)服务只能通过API/事件处理数据,没有后门;
每个(微)服务都是独立的;
所有(微)服务都符合应用的十二要素,以及其他标准。
代码格式/风格
选择一种代码风格,并贯彻到底。如果可以,在提交前自动格式化。
命名约定
选择一种约定,并贯彻到底。
API标准
需要考虑的关键事项:
遵循 REST 命名实践(名词、复数以及其他常见的标准),网上有很多指南,选择一种,并贯彻到底;
与命名规则统一。该标准适用于一切,比如负载对象命名、属性等,而不仅限于端点。无论是驼峰式、蛇式还是烤肉串式,选择一种,并贯彻到底;
POST、PUT、PATCH的响应包含有意义的主体;
使用有意义的 HTTP 状态码,不要遇到错误就返回 400;
所有端点必须返回有意义的错误情况;
提供错误列表(更多细节请参见错误处理部分的介绍);
考虑 OpenAPI,以及契约优先开发,即刚开始的时候编写 OpenAPI 契约,与(内部)消费者沟通,这样可以实现更好的并行开发;
使用有意义的描述和示例记录 OpenAPI 契约;
所有(内部)API 必须使用 CorrelationId/TraceId 的标头;
默认情况下,所有 API 输入都必须有非常严格的限制;
所有 API(内部或外部)都必须经过身份验证,最好采用实时授权;
所有 API 必须重用相同的公共数据结构:可以采用通用的地址、人、国家等,也可以定义特定于业务的数据结构;
所有 API(内部或外部)只能通过 HTTPS 公开;
考虑在 API 的响应中返回安全标头,例如:Cache-Control: no-store,Content-Security-Policy: frame-ancestors 'none',Content-Type, Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: DENY等。
内部 API 之间的通信不应该通过互联网(除非是架构有意为之,或有此类要求);
不要通过互联网公开管理端点;如果有这种要求,请使用身份验证;
确保所有 API 都会严格验证收到的请求:不允许使用未记录的 JSON 字段、拒绝格式错误的 JSON 等;
正确使用数据类型,不要将所有内容都作为字符串;
尽可能使用枚举值;
定义字符串的长度限制,定义数字的最小值/最大值;
定义每个字符串的输入限制模式;
某些属性有明确的定义,因此定义正则表达式很容易,比如国家代码一律以“[A-Z]+”开头。但有些属性很难定义正则表达式,比如考虑到所有语言中人们的姓氏,这个属性(lastName)的要求就会非常宽松。建议定义一些能够防止出现奇怪字符的模式,比如 Unicode 控制字符、分隔符或符号;推荐的正则表达式对象如下:“^[^\p{C}\p{Z}\p{So}]*[^\p{C}\p{so}]+[^\p{C}\p{Z}\p{So}]*$”,但这并不能确保程序可以免受任何类型的注射,你仍然需要很好地理解数据的去向和处理方式,但至少可以确保表情符号不会破坏你的系统。
标准日志
需要考虑的关键事项:
日志的格式:使用逗号分隔的键值对(key=value)?还是使用JSON对象?选择适合工具的格式;
日志的每一行都包含 CorrelationId/TraceId,这样可以方便工具创建仪表板;
日志中记录方便理解实际情况的信息:哪个实体?业务领域?成功了吗?失败了?
一些参考做法(https://www.javacodegeeks.com/2011/01/10-tips-proper-application-logging.html);
在实际的日志实现之上使用抽象层;例如在 Java中,使用由logback实现的slf4j;
将日志作为横切关注点,利用面向切面的程序设计,仅在异常情况下记录日志,限制敏感内容的记录;
不能将日志作为记录一切备用信息的手段,将所有请求和响应都完整地记录下来。应当谨慎地记录日志,即使在调试级别或较低级别中也应如此。
更多有关日志记录的参考信息(https://ludovicianul.github.io/2021/07/06/incomplete-list-of-security/)。
数据标准
需要考虑的关键事项:
使用现有的 ISO 标准处理广为人知的对象:货币、日期、金额等;
定义可重复使用的特定于业务的对象;
API对象、数据库实体和事件也需要采用这些标准。
处理数据
需要考虑的关键事项:
在处理数据之前清理数据,下面是一个通用的清理正则表达式:“^[^\p{C}\p{Z}\p{So}]*[^\p{C}\p{so}]+[^\p{C}\p{Z}\p{So}]*$”,虽然无法杜绝所有的问题,但是可以删除可能会导致系统崩溃的奇怪字符;
确保不会将输入数据传输到内部的访问操作,如数据库查询、命令行执行等。使用参数化的数据库查询,具体指定获取的内容与传递的内容;
当需要针对特定输入的处理施加限制时,请使用白名单(而不是黑名单);
采用防御性编程实践;
确保使用不易受到 XXE 或类似攻击的高效 XML 解析器。最好不要使用 XML 作为输入,除非上下文有这样的强制要求。
日志数据
需要考虑的关键事项:
不要记录敏感数据。如果出于某种原因仍然需要记录,请屏蔽敏感数据。这里的敏感程度取决于具体的业务与法规;
创建/使用一个库,默认屏蔽平台中最敏感的数据,例如,如果你正在处理付款,则默认屏蔽银行卡号,不要让每个开发者自行决定;
考虑在每次添加新的敏感数据时扩展该库,如果需要添加大量数据,则必须平衡性能;
日志记录库必须允许特定的配置,以便每个单独的服务可以屏蔽额外的数据,而无需扩展该库;
日志记录库必须提供清理方法(即通过调用特定的方法),确保所有情况都可以使用同一种清理技术;
日志记录库必须在记录数据之前清理数据(比如删除所有与“\p{Z}\p{C}\p{So}”相匹配的字符);
日志记录库必须删除 CR 和 LF 字符,以防止 CRLF 注入;
建立明确的日志归档策略。
存储数据
需要考虑的关键事项:
只能存储与当前上下文或可预见的将来相关的数据,数据存储不能作为以备不时之需的手段;
数据的存储必须遵守相关规定,你必须知道这一点;
某些数据不能明文存储(比如信用卡号),可以使用硬件或软件加密;
不能在纯文本文件的版本控制中存储隐私数据(密码、加密密钥、ssh 密钥、私钥等),必须使用专用产品或服务,例如 Vault、HSM等;
在执行敏感数据的加密或散列操作时,必须“加盐”和/或“加胡椒”,以防止暴力攻击;
考虑构建(或使用)令牌化敏感数据的集中式服务;
令牌化所有受某种监管的数据:银行卡号、PII 数据等。所有(微)服务都应该使用令牌(而不是实际的数据),并仅在需要时还原令牌,这可以最大限度地减少合规所需的工作,还可以更好地控制数据;
加强令牌化解决方案的安全性,不允许从外部访问其 API。
事件/消息标准
需要考虑的关键事项:
创建一个事件列表,让每个人都知道每个事件的目的;
使用事件规范进行验证;
避免使用通用事件来发送一切消息,避免在不知情的情况下泄露敏感信息;
考虑用交换令牌代替包含敏感信息的实际数据。
配置管理
需要考虑的关键事项:
避免在源文件中硬编码配置;
考虑使用集中式配置管理;
按环境分隔配置;
不要在源文件或版本控制中存储隐私数据(密码、api 密钥、ssh 密钥、私钥等),使用适当的隐私数据系统;
不要保留任何可部署单元(云服务、产品或自己的(微)服务)的默认凭据;
不要将仅用于测试的代码或配置放入生产;
不要只在服务中构建测试后门;
使用版本控制来记录配置更改;
使用适当的配置完整性检查机制。
错误处理
需要考虑的关键事项:
将异常和错误视为横切关注点,利用面向切面的程序设计,使用 ControllerAdvices 等类似的工具;
将常见的异常/错误(验证问题、未找到资源、格式错误消息)处理逻辑放入到共享库中,这可以减少(微)服务之间的交互摩擦;
编写一个错误列表;
使用错误代码(例如 MICRO-4221代表结构验证导致请求出错,MICRO-4222 代表业务验证导致请求出错);
不要在响应中泄漏内部状态,避免传递 e.getMessage(),返回的每个错误都必须由其根本原因创建,同时不要泄漏内部数据;
使用 catch-all 机制,以避免因意外异常而泄漏内部状态。你可以利用全局错误处理程序捕获异常并返回 500;
所有错误都应该返回同一个对象,以实现统一的体验;
在API文档中使用适当的 HTTP 状态码记录所有的错误情况。如果使用 OpenAPI,请记录所有可能的 HTTP 状态码,即使它们返回相同的 OpenAPI 对象。
分支策略与提交
需要考虑的关键事项:
使用简单的分支策略,基于trunk的方式、GitHub工作流等,随便选一个;
存储库和分支采用有意义的命名;
使用描述性提交,以方便将来追踪变更;
分多次提交少量的代码,以更好地分隔变更;
使用智能提交,即提供指向任务(来自任务管理系统)的链接;
考虑使用预提交钩子来验证提交;
提交消息中不要包含敏感信息;
存储库启用远程访问时需要特别小心,特别是托管在云中的存储库。
代码审查
需要考虑的关键事项:
必须执行代码审查(保持善意和自信,提供具体说明,以及其他良好的习惯);
机械的检查交给工具来做,专心审查功能以及是否符合标准和实践;
如果你反复遇到同一个问题,请将其添加到标准中;
建立检查列表,直到所有人都养成关注同一个问题的习惯。
工具与第三方库
需要考虑的关键事项:
制定引入新工具的流程。进行权衡分析,并推广到更广泛的群体,得到所有人的接受/同意,并确保覆盖更广泛的情况;
选择开源软件时注意许可;
创建许可列表,在其中列出所有无需询问即可使用的许可、需要讨论的许可以及不允许使用的许可;
在发现新工具/库/产品时,不要着急使用,你需要考虑:是否稳定?是否有良好的维护?是否有成功案例?
考虑使用 OWASP Dependency Check、License Plugin 之类的工具,或其他更复杂的工具,例如 Black Duck等;
制定一个普遍认可的工具/库创建列表,供所有人挑选;
定期更新依赖项。
代码分析
需要考虑的关键事项:
使用一种或多种工具来分析代码;
必须拥有一种(至少)常见的代码分析工具,以及一种注重安全实践的工具;
常见的(Java)代码分析工具:Sonarqube、PMD、SpotBugs;
常见的安全代码分析工具:Veracode、Checkmarx、Sonarqube;
你不需要遵循这些工具提供的所有标准规则(尽管通常这些标准都与行业建议一致),可以根据实际情况选出适合自己的规则。
测试
需要考虑的关键事项:
执行各个级别的自动化测试:单元测试、集成测试、组件测试、API测试、端到端测试等;
除了正面测试之外,还需要关注负面测试以及边界测试,CATS 是一款优秀的 API 测试工具;
不要忽视失败的测试,即使是间歇性失败的测试,其中可能隐藏了严重的潜在问题;
测试必须具有弹性,而且必须充分;
测试必须使用类似且可预测的方法;
测试不得依赖于复杂的外部设置,必须能够通过模拟依赖项、使用内存设置来完成测试,或采用测试容器,或仅依赖于已部署的(微)服务。任何其他步骤只会使设置更加复杂化;
考虑在管道内添加一些安全测试;
考虑突变测试。
CI/CD
需要考虑的关键事项:
在重要的环节添加质量检测,并作为检查点,如果检测失败,则构建也会失败;
质量检测必须符合这些标准,并自动检查每个(微)服务;
下面是一个 CI/CD 管道示例:
编译和构建;
检查格式;
运行测试并检查覆盖率;
运行突变测试;
运行代码分析;
运行安全代码分析;
检查第三方库的漏洞;
检查第三方库的许可;
部署;
运行 API 测试;
运行其他类型的测试。
虽然看上去很冗长,但在微服务上运行这些检查非常快;
编写自己的流水线脚本;
不要将流水线与(微)服务融合到一起;
使用适合于所有(微)服务的流水线模板。
认证和授权
需要考虑的关键事项:
不要自行构建身份验证和授权,请使用标准的产品和服务;
所有内部与外部API都需要身份认证,选择一些可靠的认证服务;
外部和内部调用需要使用单独的身份验证和授权机制:使用一组凭据/机制来验证外部调用,再使用另一组单独的凭据/机制来验证内部调用;
凭据始终需要加密,无论是否投入了使用;
所有API(无论内部还是外部)都需要使用 HTTPS;
不要通过 HTTP GET 接收身份验证凭据,仅使用 HTTP 头部或 HTTP POST/PUT;
即使在调试时也不能记录凭据,通过日志记录的catch all来避免记录凭据;
确保授权和身份验证机制允许细粒度的控制和管理,即可以限制每个操作的调用次数、撤销访问、颁发额外的凭据等;
考虑使用集中式身份提供程序和公共库;
对于高度敏感的 API/服务使用增强的安全控制(API 的双向 TLS,服务访问的 MFA);
使用随机数来防止重放攻击;
设计和构建采用最低特权原则。
通用安全实践
需要考虑的关键事项:
永远不要使用自己的加密程序,不要在这个空间内重新发明轮子;
使用行业推荐算法:AES 256、RSA 2048+、SHA-2 512等;
使用 TLS 1.3+ 来保证传输安全;
在执行敏感数据的加密或散列操作时,必须“加盐”和/或“加胡椒”,以防止暴力攻击;
检查编程语言处理敏感信息的实践,例如在 Java 中,在处理密码、银行卡号、社会保险号时,必须使用 byte[],不能使用 String。必须尽量减少数据在内存中停留的时间并在使用后清除对象。
质量
如上所述,系统生命周期的标准和实践并不一定会直接关系到安全。质量方面也有这个问题。当前的设计和方法缺陷可能会导致应用程序宕机,即便并不是由真正的安全问题引起的。
关于性能,需要考虑的关键事项:
对于昂贵的资源,比如数据库、API等,应该使用连接池;
使用线程池;
使用缓存;
操作数据时使用适当的集合;
如果可以,请使用并行编程;
确保你了解对象关系映射(ORM)如何生成查询;
避免将大量资源加载到内存,请使用数据流;
建立每个(微)服务实例的性能基准,帮助你了解何时需要扩展;
定期执行负载测试与性能测试。
关于弹性,需要考虑的关键事项:
使用断路器、重试、超时、速率限制;
当依赖的 API 不可用时有明确的回退策略;
关于弹性的资源:
https://engineering.grab.com/designing-resilient-systems-part-1
https://engineering.grab.com/designing-resilient-systems-part-2
所有 API 都必须具备幂等性;
不要在(微)服务实例中存储状态,可以考虑使用分布式缓存。
关于可用性和可扩展性,需要考虑的关键事项:
设计不应当限制(微)服务的水平扩展;
做好故障计划,建立自动化的机制,根据负载水平自动伸缩;
考虑分片,只读副本;
使用多区域部署。
关于可观察性和监控,需要考虑的关键事项:
所有(微)服务都必须公开有关应用程序以及底层容器的健康端点;
健康端点必须返回有关其所有依赖项的信息:数据库、加密服务、连接的 API、事件总线等。
利用标准化日志创建有意义的操作仪表板。
自动化
一切都应尽可能实现自动化。自动化可以确保预测性和一致性。应该利用 CI/CD 流水线自动化所有检查,这些检查将从质量的角度评估(微)服务。对于不适合自动化的标准,可以考虑 Semgrep 等工具。
总结
在本文中,我尽可能地列出了方方面面的实践建议。虽然这不是一份详尽的攻略,但可以作为建立安全思维模式的起点。在贯彻完成这些实践后,你可以进一步深入的研究。这些实践不仅可以确保系统安全,而且还具备结构化与统一性。在快速开发系统(无论是开发全新的系统还是改造遗留系统)时,这些实践尤为重要。你不需要从第一天开始就贯彻所有实践,特别是过多的通用标准会限制你的选择。你可以逐步尝试,然后看看效果。
原文链接:https://ludovicianul.github.io/2021/07/06/incomplete-list-of-security/
声明:本文由CSDN翻译,转载请注明来源。